package ar.unc.famaf.pln.giveselector.builders;

import java.io.*;
import java.util.*;
import java.util.zip.GZIPOutputStream;

import ar.unc.famaf.pln.giveselector.context.*;
import ar.unc.famaf.pln.giveselector.selectors.ACLSelector;
import ar.unc.famaf.pln.giveselector.utils.GIVEConnector;

import give.comm.*;
import give.formula.*;
import give.mapreplay.GameEvent;
import give.nlgserver.discretize.*;
import give.nlgserver.planning.*;
import give.world.*;

/**
 * A DefaultUtteranceBuilder builds an annotated corpus. It has been separated
 * from the DefaultComparisonSelector for clarity purposes.
 * @author Alexandre Denis
 * @author Luciana Benotti
 */
public class ACLBuilder implements IBuilder
{
	protected GIVEConnector connector;
	protected PlannerInterface planner;
	
	// The Database information (url, username, passw) should be here.
	protected static final String MATCHMAKER_FILE = "datas/aclbuilder-match-config.xml";
	// The output. It will content the serialization of the 'utterances' object.
	protected static final String ANNOTATION_FILE = "datas/action-utterances-with-id.bin.gz";
	
	// Change this if you want to redirect the std output to a file
	protected static final boolean REDIRECT_STD_OUTPUT = false;
	protected static final String STD_OUTPUT_FILE = "datas/OUTPUT.DAT";

	// <world name, utterances>
	protected Map<String, Set<Utterance>> utterances = new HashMap<String, Set<Utterance>>();
	
	protected static Integer numberOfUtterances = 0;
	protected static Integer numberOfFailedUtterances = 0;


	/**
	 * Creates the annotated corpus.
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException
	{
		// Redirecting standard output to a file
		if (REDIRECT_STD_OUTPUT) 
		{
			System.setOut(
				new PrintStream(
					new BufferedOutputStream(
						new FileOutputStream(STD_OUTPUT_FILE))));
		}

		ACLBuilder builder = new ACLBuilder();
		System.out.println(builder);
		ACLBuilder.save(new File(ANNOTATION_FILE), builder.getUtterances());
	}


	/**
	 * Creates an InactionUtteranceBuilder based on all valid games 
	 */
	public ACLBuilder()
	{
		connector = new GIVEConnector(MATCHMAKER_FILE);
		List<Integer> ids = connector.getGameIds();
		System.out.println(ids);
		
		for (int i = 0;i < ids.size(); i++)
		{
			int gameId = ids.get(i);
			System.out.println("\nProcessing game " + gameId + " (" + (i+1) + "/" + ids.size() + ")");
			World world = connector.getWorld(gameId);
			planner = new LazyFfPlannerInterface(new RegionDiscretizer(world));
			computeActionUtterances(world, gameId);
		}
	}


	/**
	 * Creates an InactionUtteranceBuilder based on given game id.
	 */
	public ACLBuilder(int gameId)
	{
		connector = new GIVEConnector(MATCHMAKER_FILE);
		World world = connector.getWorld(gameId);
		planner = new LazyFfPlannerInterface(new RegionDiscretizer(world));
		computeActionUtterances(world, gameId);
	}
	
	/**
	 * @return The annotated corpus as sets of utterances
	 */
	public Map<String, Set<Utterance>> getUtterances()
	{
		return utterances;
	}

	/**
	 * Given an action message from the log, it gets the associated action 
	 * and then applies it to the world. Adds the action to the context's
	 * past action list. Adds the action to the response object.
	 */
	public static void processActionMessage(World w, ActionMessage aMsg, Context c, Response r) 
	{
		aMsg.findAction(w); // It's necesary to call findAction() for getAction() to work properly
		Action a = aMsg.getAction();
		
		Atom header = aMsg.getHeader();
		a.apply(header, w);
		
		r.addAction(header);
		c.addPastAction(aMsg);
	}
	
	/**
	 * Given a status message from the log, it updates the world state
	 * by updating the player pos and or. It adds a new move action 
	 * to the response and context objects only only if the player has moved 
	 * to another region. It updates the player or in the context object.
	 * @return True if the player has executed a move action.
	 */
	public static boolean processStatusMessage(World w, StatusMessage sMsg, 
			Context c, Response r, RegionDiscretizer rd) 
	{
		w.setPlayerOrientation( sMsg.getOrientation() );
		w.setPlayerPosition( sMsg.getPosition() );
		
		r.updateMove(c.getRegion(), getPlayerRegion(rd, w));
	
		boolean moved = c.updateMove(getPlayerRegion(rd, w));
		c.setOrientation(w.getPlayerOrientation());
		
		return moved;
	}
	
	/**
	 * Checks if the given Utterance has failed in instructing the DF.
	 * An utt is considered a failure if the player reaction if different
	 * from the utt expected reaction (semantics of the utt)
	 * Increments the number of Utterances counters. 
	 */
	protected static void classifyUtterance(Utterance utt)
	{
		numberOfUtterances++;
		List<Atom> expectedReaction = ACLSelector.cleanPlannerActions(utt.getResponse().getDiscretizedActions());
		List<Atom> playerReaction = ACLSelector.cleanPlannerActions(utt.getResponse().getExecutedActions());

		if ( ( expectedReaction.isEmpty() && !playerReaction.isEmpty() ) 
		|| (!expectedReaction.isEmpty() && playerReaction.isEmpty() ) )
		{
			numberOfFailedUtterances++;
			System.out.println("Failed Utterance: "+ utt.getText());
		}
		else if (!expectedReaction.isEmpty() && !playerReaction.isEmpty())
		{
			if (!expectedReaction.get(0).equals(playerReaction.get(0)))
			{
				numberOfFailedUtterances++;
				System.out.println("Failed Utterance: "+ utt.getText());
			}
		}
		System.out.println();
	}
	
	/**
	 * Prints some Utterance information with an specific format.
	 * @param utt
	 * @param timeStamp
	 */
	protected static void printUtteranceInfo(Utterance utt, Date timeStamp) 
	{
		System.out.println("Utterance: " + "[" + utt.getId() + "] " + utt.getText());
		System.out.println("Timestamp: " + timeStamp);
		System.out.println(utt.getResponse());
	}
	
	/**
	 * Computes a new plan for the given world and goals
	 */
	public static List<Atom> getPlan(PlannerInterface planner, World w, List<Formula> goals)
	{
		List<Atom> plan = null;
		try {
			plan = planner.computePlan(w, goals);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return plan;
	}
	
	/**
	 * Calculates the goals of the given world without including the position atoms
	 * @return A list of formulaes without "position" and "other-position" atoms
	 * which are the Goals of the world w
	 */
	public static List<Formula> getGoalsWithoutPos(World w)
	{
		World goalState = new World();
		w.copyTo(goalState);
		List<Atom> goalAtoms = new ArrayList<Atom>(goalState.getTrueAtoms());
		
		Collection<Atom> positions = new ArrayList<Atom>();
		
		for (Atom atom:goalAtoms)
		{
			if ((atom.getPredicate().equals("position"))
			|| (atom.getPredicate().equals("other-position")))
				positions.add(atom);
		}
		goalAtoms.removeAll(positions);
		List<Formula> goalFormula = new ArrayList<Formula>(goalAtoms);
		
		return goalFormula;
	}

	/**
	 * Uses the given planner to compute a new plan which starting
	 * state is given by the world initialState and final state is given
	 * by the world finalState.
	 * @return a new plan
	 */
	public static List<Atom> computeDiscretizedResponse(boolean isLastText, 
			PlannerInterface planner, World initialState, World finalState)
	{
		List<Formula> goalFormula = getGoalsWithoutPos(finalState);
		
		// the action take trophy is not reported as an action message so we need to do the following
		List<Atom> discretizedResponse = 
			(isLastText) ?
			  getPlan(planner, initialState, finalState.getGoals()) 
			: getPlan(planner, initialState, goalFormula);
	    return discretizedResponse;
	}

	/**
	 * Associates to each utterance, the next actions the player has to do
	 * according to the current plan.
	 */
	public void computeActionUtterances(World w, int gameId)
	{
		World world = new World();
		w.copyTo(world);

		RegionDiscretizer discretizer = new RegionDiscretizer(w);
		Context context = new Context(getPlayerRegion(discretizer, world));
		Context currentCxt = null;
		
		String currentTextMessage = null; 	// The Old log text message
		Utterance currentUtterance = null;	// The Utt that will be created
		Response response = new Response();
		
		World initialState = null;
		
		Date currentTimeStamp = null;
		Date newTimeStamp = null;

		List<GameEvent> events = connector.getGameEvents(gameId);

		for(int i = 0; i < events.size(); i++)
		{
			GameEvent event = events.get(i);
			Message message = event.message;
			currentTimeStamp = newTimeStamp;
			newTimeStamp = event.timestamp;

			// update the world when an action is done
			if (message instanceof ActionMessage)
				processActionMessage(world, (ActionMessage) message, context, response);
			
			else if (message instanceof StatusMessage)
				processStatusMessage(world, (StatusMessage) message, context, response, discretizer);
			
			else if (message instanceof TextMessage)
			{
				String nextText = ((TextMessage) message).getText();				
				List<Atom> plan = getPlan(planner, world, world.getGoals());
				context.setFutureActions(plan);

				if (initialState != null && currentCxt != null)
				{
					if (plan != null && !plan.isEmpty())
					{
							
						List<Atom> discretizedResponse = computeDiscretizedResponse(
								nextText.contains("Yay! You successfully completed the task! ... 3"), 
								planner, initialState, world);
						if (discretizedResponse != null)
						{
							response.setDiscretizedActions(discretizedResponse);
							currentUtterance = new Utterance(currentTextMessage, new Context(currentCxt), response);
							response = new Response();
						}
						else
							System.err.println("No plan for utterance: " + currentUtterance);
					}
					else
					{
						// if we cannot find a plan we cannot use that game
						System.out.println("Error: null plan for '" + nextText + "' ??");
						System.out.println("Game " + gameId + " discarded");
						break;
					}
				}
							
				if (currentTextMessage != null)
				{
					printUtteranceInfo(currentUtterance, currentTimeStamp);
					classifyUtterance(currentUtterance);
					addUtterance(world.getName(), currentUtterance);
				}
				currentTextMessage = nextText;
				initialState = new World();
				world.copyTo(initialState);
				currentCxt = new Context(context);
			}
			
		}
		
		// The last Utterance
		if (currentUtterance!=null)
		{
			//classifyUtterance(currentUtterance);
			printUtteranceInfo(currentUtterance, currentTimeStamp);
			addUtterance(world.getName(), currentUtterance);
		}
	}


	/**
	 * Returns the region the player is in.
	 * @param discretizer
	 * @param world
	 * @return
	 */
	public static String getPlayerRegion(RegionDiscretizer discretizer, World world)
	{
		return discretizer.getPositionName(world.getPlayerPosition());
	}


	/**
	 * Saves this InactionUtteranceBuilder to given file.
	 * @param fileName
	 * @throws FileNotFoundException
	 */
	public static void save(File file, Map<String, Set<Utterance>> utterances)
	{
		if (file.exists())
			file.delete();

		FileOutputStream fos = null;
		GZIPOutputStream gos = null;
		ObjectOutputStream out = null;
		try
		{
			fos = new FileOutputStream(file);
			gos = new GZIPOutputStream(fos);
			out = new ObjectOutputStream(gos);
			out.writeObject(utterances);
			out.close();
		}
		catch (IOException ex)
		{
			ex.printStackTrace();
		}
	}


	/**
	 * Adds the given association action-text.
	 * @param action
	 * @param text
	 */
	protected void addUtterance(String worldName, Utterance utt)
	{
		if (!utterances.containsKey(worldName))
			utterances.put(worldName, new HashSet<Utterance>());

		utterances.get(worldName).add(utt);
	}


	/**
	 * Returns a String representing all associations between actions and
	 * utterances.
	 */
	public String toString()
	{
		StringBuilder ret = new StringBuilder();
		for(String worldName : utterances.keySet())
		{
			ret.append("\n\nWorld " + worldName + "\n\n");
			for(Utterance utt : utterances.get(worldName))
				ret.append(utt + "\n");
		}
		ret.append("\nUtterances: "+ numberOfUtterances + "\n");
		ret.append("\nUtterances Failed: "+ numberOfFailedUtterances + "\n");
		return ret.toString();
	}
}
